#
# Ronin Exploits - A Ruby library for Ronin that provides exploitation and
# payload crafting functionality.
#
# Copyright (c) 2007-2013 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# This file is part of Ronin Exploits.
#
# Ronin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ronin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ronin.  If not, see <http://www.gnu.org/licenses/>
#

require 'ronin/database/migrations/exploits/exploit'
require 'ronin/exploits/exceptions/unknown_helper'
require 'ronin/exploits/exceptions/target_unspecified'
require 'ronin/exploits/exceptions/target_data_missing'
require 'ronin/exploits/exceptions/restricted_char'
require 'ronin/exploits/reference'
require 'ronin/exploits/target'
require 'ronin/exploits/helpers'
require 'ronin/exploits/tests'
require 'ronin/payloads/payload'
require 'ronin/payloads/has_payload'
require 'ronin/post_exploitation'
require 'ronin/advisory'
require 'ronin/script'
require 'ronin/behaviors/testable'
require 'ronin/behaviors/buildable'
require 'ronin/behaviors/deployable'
require 'ronin/extensions/kernel'

require 'set'
require 'chars/char_set'

module Ronin
  module Exploits
    #
    # The {Exploit} class allows for describing exploits for security
    # vulnerabilities, purely in Ruby. Exploits contain metadata about the
    # exploit/vulnerability and methods which defines the functionality
    # of the exploit. Exploits may also be coupled with a payload, which
    # can be used with the exploit.
    #
    # # Metadata
    # 
    # An {Exploit} is described via metadata, which is cached into the
    # Ronin Database. The cacheable metadata must be defined within a
    # `cache` block, so that the metadata is set only before the exploit is
    # cached:
    #
    #     cache do
    #       self.name = 'FailTTPd 0.2.6b Buffer Overflow'
    #       self.version = '0.2'
    #       self.description = %{
    #         FailHTTPd 0.2.6b contains a buffer overflow in it's handling
    #         of the TRACE request.
    #       }
    #
    #       # ...
    #     end
    #
    # ## License
    #
    # An {Exploit} may associate with a specific software license
    # using the `licensed_under` method:
    #
    #     cache do
    #       # ...
    #
    #       licensed_under :cc_sa_by
    #     end
    #
    # ## Authors
    #
    # An {Exploit} may have one or more authors which contributed to the
    # exploit, using the `author` method:
    #
    #     cache do
    #       # ...
    #
    #       author name: 'evoltech', organization: 'HackBloc'
    #       author name: 'postmodern', organization: 'SophSec'
    #     end
    #
    # ## Status
    #
    # An {Exploit} has a specific development status, which describes
    # the reliability of the exploit:
    #
    #     cache do
    #       # ...
    #
    #       self.status = :proven
    #     end
    #
    # The {#status} property may be one of the following values:
    #
    # * `potential`
    # * `proven`
    # * `weaponized`
    #
    # ## Disclosure
    #
    # An {Exploit} may specify whether it has been released publically.
    #
    #     cache do
    #       # ...
    #
    #       self.released = true
    #     end
    #
    # {#released} defaults to `false`.
    #
    # An {Exploit} may also specify whether it has been reported.
    #
    #     cache do
    #       # ...
    #
    #       self.released = false
    #       self.reported = true
    #     end
    #
    # {#reported} defaults to `false`.
    #
    # ## Advisories
    #
    # An {Exploit} may list the {Advisory advisories} that describe the
    # vulnerability.
    #
    #     cache do
    #       # ...
    #
    #       advisory 'CVE-2011-1234',
    #                'OSVDB-4567'
    #     end
    #
    # ## Targets
    #
    # An {Exploit} may have one or more targets, which discribe the
    # Architectures, Operating Systems and Products the exploit targets.
    # Each target may also define extra data that is specific to that
    # target.
    #
    #     cache do
    #       # ...
    #
    #       targeting do |t|
    #         t.arch! :i686
    #         t.os! name: 'Linux'
    #         t.software! name: 'FailTTPd', version: '0.2.6b'
    #
    #         # target specific data
    #         t.data = {value1: 0x03c1, value2: 0xa0}
    #       end
    #     end
    #
    # # Methods
    #
    # The functionality of an {Exploit} is defined by four main methods:
    #
    # * `build` - Handles building the exploit.
    # * `test` - Optional method which handles testing a built
    #   exploit, before it is deployed.
    # * `deploy` - Handles deploying a built and tests exploit
    #   against a host.
    # * `evacuate` - Handles cleaning up after a deployed exploit.
    #
    # The `build`, `test`, `deploy`, `evacuate` methods can be invoked
    # individually using the `build!`, `test!`, `deploy!`, `evacuate!`
    # methods, respectively. Additionally, the `exploit!` method will
    # accept additional parameter values and will call `build!`,
    # `test!`, `deploy!`, in that order.
    #
    # # Exploit/Payload Coupling
    #
    # An {Exploit} may also couple with a payload, which will be built,
    # tested and deployed along with the exploit. When a payload is
    # coupled with an exploit, the `payload` instance variable of the
    # exploit will contain the new payload.
    #
    # To use a cached payload, from the Ronin Database, simply use the
    # {#use_payload!} method:
    #
    #     exploit.use_payload!(name.like => '%Bind Shell%')
    #
    # In order to use a payload, loaded directly from a file, call the
    # {#use_payload_from!} method:
    #
    #     exploit.use_payload_from!('path/to/my_payload.rb')
    #
    # Before an exploit is built, the payload will be built and encoded.
    # The built payload will be stored in the {#raw_payload} instance
    # variable.
    #
    # After an exploit is deployed, the payload will also be deployed,
    # ensuring that any post-deploy functionality is triggered. Before the
    # exploit is cleaned up after, the payload will be cleaned up.
    #
    class Exploit

      include Script
      include Behaviors::Testable
      include Behaviors::Buildable
      include Behaviors::Deployable
      include Payloads::HasPayload
      include PostExploitation::Mixin
      include Tests

      # Primary key of the exploit
      property :id, Serial

      # The status of the exploit (either, 'potential', 'proven' or
      # 'weaponized')
      property :status, String, set: [
        'potential',
        'proven',
        'weaponized'
      ], default: 'potential'

      # Specifies whether the exploit has been released
      property :released, Boolean, default: false

      # Specifies whether the exploit has been reported
      property :reported, Boolean, default: false

      # Advisory References
      has 0..n, :references

      # Advisories for the vulnerability
      has 0..n, :advisories, through: :references,
                             model:   'Ronin::Advisory'

      # Targets for the exploit
      has 0..n, :targets

      # The helpers used by the exploit
      attr_reader :helpers

      # Exploit target
      attr_writer :target

      # Characters to restrict
      attr_reader :restricted_chars

      # Encoders to run on the payload
      attr_reader :encoders

      # The raw unencoded payload
      attr_reader :raw_payload

      #
      # Creates a new Exploit object.
      #
      # @param [Hash] attributes
      #   Additional attributes used to initialize the exploit's model
      #   attributes and parameters.
      #
      def initialize(attributes={})
        super(attributes)

        @helpers = Set[]

        @target = nil
        @restricted_chars = Chars::CharSet.new
        @encoders = []
      end

      #
      # Searches for exploits for the given advisories.
      #
      # @param [Array<String>] identifiers
      #   Advisory identifier Strings.
      #
      # @return [DataMapper::Collection<Ronin::Exploits::Exploit>]
      #   The exploits for the given advisories.
      #
      # @example
      #   Exploit.advisory('CVE-2011-1234', 'OSVDB-1234')
      #
      # @since 1.0.0
      #
      def self.advisory(*identifiers)
        query = all

        identifiers.each do |identifier|
          publisher, year, number = Advisory.split(identifier)

          query |= all(
            'advisories.publisher' => publisher,
            'advisories.year'      => year,
            'advisories.number'    => number
          )
        end

        return query
      end

      #
      # Finds all exploits which target a given architecture.
      #
      # @param [String, Symbol] name
      #   The name of the architecture.
      #
      # @return [Array<Exploit>]
      #   The exploits targeting the architecture.
      #
      def self.targeting_arch(name)
        all('targets.arch.name' => name.to_s)
      end

      #
      # Finds all exploits which target a given OS.
      #
      # @param [String, Symbol] name
      #   The name of the OS.
      #
      # @return [Array<Exploit>]
      #   The exploits targeting the OS.
      #
      def self.targeting_os(name)
        all('targets.os.name' => name.to_s)
      end

      #
      # Finds all exploits which target a given software.
      #
      # @param [String, Symbol] name
      #   The name of the software.
      #
      # @return [Array<Exploit>]
      #   The exploits targeting the software.
      #
      def self.targeting_software(name)
        all('targets.software.name.like' => "%#{name}%")
      end

      #
      # Adds a new reference to an {Advisory}.
      #
      # @param [Array<String>] identifiers
      #   Advisory identifier Strings.
      #
      # @return [DataMapper::Collection<Ronin::Advisory>]
      #   The advisories of the exploit.
      #
      # @raise [ArgumentError]
      #   The publisher name for the advisory was unknown.
      #
      # @example
      #   advisory 'CVE-2011-1234',
      #            'OSVDB-1234'
      #
      # @since 1.0.0
      #
      def advisory(*identifiers)
        identifiers.each do |identifier|
          self.advisories << Advisory.parse(identifier)
        end

        return self.advisories
      end

      #
      # Adds a new target to the exploit.
      #
      # @param [Hash] attributes
      #   Additional attributes to create the target with.
      #
      # @yield [target]
      #   If a block is given, it will be passed the newly created target.
      #
      # @yieldparam [Target] target
      #   The newly created target.
      #
      # @example
      #   targeting do |t|
      #     t.arch! :i686
      #     t.os! name: 'Linux'
      #   end
      #
      def targeting(attributes={},&block)
        self.targets << self.targets.model.new(attributes,&block)
      end

      #
      # Adds new characters to the list of restricted characters.
      #
      # @param [Array<String>] chars
      #   The character to restrict.
      #
      # @return [Array<String>]
      #   The new list of restricted characters.
      #
      # @example
      #   restrict 0x00, "\n"
      #   # => #<Chars::CharSet: {"\0", "\n"}>
      #
      def restrict(*chars)
        @restricted_chars += chars
      end

      #
      # Adds a new encoder to the list of encoders to use for encoding the
      # payload.
      #
      # @param [#encode] encoder
      #   The payload encoder object to use.
      #   Must provide an encode method.
      #
      # @yield [payload]
      #   If a block is given, and an encoder object is not, the block will
      #   be used to encode the payload.
      #
      # @yieldparam [String] payload
      #   The payload to be encoded.
      #
      # @return [Array]
      #   The new list of encoders to use to encode the payload.
      #
      # @raise [ArgumentError]
      #   The payload encoder object does not provide an encode method.
      #   Either a payload encoder object or a block can be given.
      #
      # @example
      #   exploit.encode_payload(some_encoder)
      #
      # @example
      #   exploit.encode_payload do |payload|
      #     # ...
      #   end
      #
      def encode_payload(encoder=nil,&block)
        if encoder
          unless encoder.respond_to?(:encode)
            raise(ArgumentError,"The payload encoder must provide an encode method")
          end

          @encoders << encoder
        elsif (encoder.nil? && block)
          @encoders << block
        else
          raise(ArgumentError,"either a payload encoder or a block can be given")
        end
      end

      #
      # Finds the targets for the specific Architecture.
      #
      # @param [Arch, Symbol, #to_s] arch
      #   The architecture to search for.
      #
      # @return [Array<Target>]
      #   The matching targets.
      #
      # @since 1.0.0
      #
      # @api public
      #
      def targeting_arch(arch)
        selector = case arch
                   when Arch
                     lambda { |target| target.arch == arch }
                   when Symbol
                     unless Arch.methods(false).include?(arch)
                       raise(ArgumentError,"unknown arch: #{arch}")
                     end

                     arch = Arch.send(arch)

                     lambda { |target| target.arch == arch }
                   else
                     arch = arch.to_s

                     lambda { |target| target.arch.name == arch }
                   end

        self.targets.select(&selector)
      end

      #
      # Finds the targets for the specific Operating System.
      #
      # @param [OS, Symbol, #to_s] os
      #   The Operating System to search for.
      #
      # @param [#to_s] version
      #   Additional version to search for.
      #
      # @return [Array<Target>]
      #   The matching targets.
      #
      # @since 1.0.0
      #
      # @api public
      #
      def targeting_os(os,version=nil)
        selector = case os
                   when OS
                     lambda { |target| target.os == os }
                   when Symbol
                     unless OS.methods(false).include?(os)
                       raise(ArgumentError,"unknown os: #{os}")
                     end

                     os = OS.send(os)

                     lambda { |target| target.os == os }
                   else
                     os = os.to_s

                     if version
                       version = version.to_s

                       lambda { |target|
                         (target.os.name == os) &&
                         (target.os.version == version)
                       }
                     else
                       lambda { |target| target.os.name == os }
                     end
                   end

        self.targets.select(&selector)
      end

      #
      # Finds the targets for the specific Software.
      #
      # @param [Software, #to_s] software
      #   The Software to search for.
      #
      # @param [#to_s] version
      #   Additional version to search for.
      #
      # @return [Array<Target>]
      #   The matching targets.
      #
      # @since 1.0.0
      #
      # @api public
      #
      def targeting_software(software,version=nil)
        selector = case software
                   when Software
                     lambda { |target| target.software == software }
                   else
                     software = software.to_s

                     if version
                       version = version.to_s

                       lambda { |target|
                         target.software.name.include?(software) &&
                         (target.software.version == version)
                       }
                     else
                       lambda { |target|
                         target.software.name.include?(software)
                       }
                     end
                   end

        self.targets.select(&selector)
      end

      #
      # Selects a target to use in exploitation.
      #
      # @param [Hash] options
      #   Target query options.
      #
      # @option options [Arch, Hash, Symbol, String] :arch
      #   The targeted Architecture.
      #
      # @option options [OS, Hash, Array<String, String>, Symbol, String] :os
      #   The targeted Operating System.
      #
      # @option options [Software, Hash, Array<String, String>, String] :software
      #   The targeted Software.
      #
      # @return [Target]
      #   The selected target.
      #
      # @raise [TargetUnspecified]
      #   No matching target could be found.
      #
      # @example Targetting an Architecture
      #   use_target! arch: :i686
      #
      # @example Targetting an Operating System
      #   use_target! os: 'Linux'
      #
      # @example Targetting an Operating System and version
      #   use_target! os: ['Linux', '2.6.24']
      #
      # @example Targetting the Software
      #   use_target! software: 'Lucene'
      #
      # @example Targetting the Software name and version
      #   use_target! software: ['Lucene', '1.8.0']
      #
      # @since 0.3.0
      #
      def use_target!(options={})
        query = self.targets

        if options[:arch]
          query = (query & targeting_arch(options[:arch]))
        end

        if options[:os]
          query = (query & targeting_os(*options[:os]))
        end

        if options[:software]
          query = (query & targeting_software(*options[:software]))
        end

        unless (@target = query.first)
          raise(TargetUnspecified,"could not find any matching targets")
        end

        return @target
      end

      #
      # @return [Target]
      #   The current target to use in exploitation.
      #
      def target
        @target ||= self.targets.first
      end

      #
      # @return [Arch]
      #   The current targeted architecture.
      #
      def arch
        target.arch if target
      end

      #
      # @return [OS]
      #   The current targeted OS.
      #
      def os
        target.os if target
      end

      #
      # @return [Software]
      #   The current targeted software.
      #
      def software
        target.software if target
      end

      #
      # Associates a payload with the exploit, and the exploit with the
      # payload.
      #
      # @param [Payload] new_payload
      #   The new payload to associate with the exploit.
      #
      # @return [Payload]
      #   The new payload.
      #
      # @since 0.3.0
      #
      def payload=(new_payload)
        if (@payload && new_payload.nil?)
          @payload.exploit = nil
        end

        super(new_payload)

        if @payload
          print_info "Using payload: #{new_payload}"

          @payload.exploit = self
        end

        return @payload
      end

      #
      # Sets the raw payload to use with the exploit.
      #
      # @param [String, #to_s] new_raw_payload
      #   The new raw payload to use with the exploit.
      #
      # @return [String]
      #   The new raw payload of the exploit.
      #
      def raw_payload=(new_raw_payload)
        new_raw_payload = new_raw_payload.to_s

        print_debug "Using raw payload: #{new_raw_payload.dump}"

        @raw_payload = new_raw_payload
      end

      #
      # Builds the current payload, saving the result to the `@raw_payload`
      # instance variable.
      #
      # @param [Hash] options
      #   Additional options to build the paylod with.
      #
      # @return [String]
      #   The built payload.
      #
      # @see Payload#build!
      # @since 0.3.0
      #
      def build_payload!(options={})
        if @payload
          @raw_payload = ''

          @payload.build!(options)
          @raw_payload = @payload.raw_payload
        else
          @raw_payload ||= ''
        end

        return @raw_payload
      end

      #
      # Encodes the current payload and makes the result available via
      # the {#raw_payload} instance variable.
      #
      # @return [String]
      #   The encoded payload.
      #
      def encode_payload!
        @raw_payload = @raw_payload.to_s

        @encoders.each do |encoder|
          print_debug "Encoding payload: #{@raw_payload.dump}"

          new_payload = if encoder.respond_to?(:encode)
                          encoder.encode(@raw_payload)
                        elsif encoder.respond_to?(:call)
                          encoder.call(@raw_payload)
                        end

          @raw_payload = (new_payload || @raw_payload).to_s
        end

        return @raw_payload
      end

      #
      # Builds the exploit and checks for restricted characters or
      # patterns.
      #
      # @param [Hash] options
      #   Additional options to also use as parameters.
      #
      # @see build
      #
      def build!(options={},&block)
        build_payload!(options)
        encode_payload!

        super(options,&block)
      end

      #
      # Verifies then deploys the exploit. If a payload has been set,
      # the payload will also be deployed.
      #
      # @yield [exploit]
      #   If a block is given, it will be passed the deployed exploit.
      #
      # @yieldparam [Exploit] exploit
      #   The deployed exploit.
      #
      # @return [Exploit]
      #   The deployed exploit.
      #
      # @raise [ExploitNotBuilt]
      #   The exploit has not been built, and cannot be deployed.
      #
      # @see deploy
      #
      def deploy!(&block)
        super do
          @payload.deploy!() if @payload
          yield self if block_given?
        end
      end

      #
      # Cleans up after the deployed exploit. If a payload has been set,
      # the payload will be cleaned up before the exploit.
      #
      # @yield [exploit]
      #   If a block is given, it will be passed the exploit before it has
      #   been cleaned up.
      #
      # @yieldparam [Exploit] exploit
      #   The exploit before it has been cleaned up.
      #
      # @return [Exploit]
      #   The cleaned up exploit.
      #
      # @since 1.0.0
      #
      def evacuate!
        super do
          @payload.evacuate!() if @payload
          yield self if block_given?
        end
      end

      #
      # Builds, tests and then deploys the exploit.
      #
      # @param [Hash] options
      #   Additional options to build the exploit with.
      #
      # @option options [Boolean] :dry_run (false)
      #   Specifies whether to do a dry-run of the exploit, where the
      #   exploit will be built, tested but *not* deployed.
      #
      # @yieldparam [Exploit] exploit
      #   The deployed exploit.
      #
      # @return [Exploit]
      #   The deployed exploit.
      #
      # @return [Exploit]
      #   The deployed exploit.
      #
      # @since 0.3.0
      #
      def exploit!(options={},&block)
        build!(options)

        unless options[:dry_run]
          deploy!(&block)
        end

        return self
      end

      protected

      #
      # Loads a helper module from `ronin/exploits/helpers` and extends
      # the exploit with it.
      #
      # @param [Symbol, String] name
      #   The underscored name of the exploit helper to load and extend the
      #   exploit with.
      #
      # @return [Boolean]
      #   Specifies whether the exploit helper was successfully loaded.
      #   Returns `false` if the exploit helper has already been loaded.
      #
      # @raise [UnknownHelper]
      #   No valid helper module could be found or loaded with the similar
      #   name.
      #
      # @example
      #   helper :buffer_overflow
      #
      def helper(name)
        name = name.to_sym

        return false if @helpers.include?(name)

        unless (helper_module = Helpers.require_const(name))
          raise(UnknownHelper,"unknown helper #{name}")
        end

        unless helper_module.kind_of?(Module)
          raise(UnknownHelper,"unknown helper #{name}")
        end

        @helpers << name
        extend helper_module
        return true
      end

    end
  end
end
